// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Implementation notes about interactions with VideoCaptureImpl.
//
// How is VideoCaptureImpl used:
//
// VideoCaptureImpl is an IO thread object while VideoCaptureImplManager
// lives only on the render thread. It is only possible to access an
// object of VideoCaptureImpl via a task on the IO thread.
//
// How is VideoCaptureImpl deleted:
//
// A task is posted to the IO thread to delete a VideoCaptureImpl.
// Immediately after that the pointer to it is dropped. This means no
// access to this VideoCaptureImpl object is possible on the render
// thread. Also note that VideoCaptureImpl does not post task to itself.
//
// The use of Unretained:
//
// We make sure deletion is the last task on the IO thread for a
// VideoCaptureImpl object. This allows the use of Unretained() binding.

#include "content/renderer/media/video_capture_impl_manager.h"

#include <algorithm>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/child/child_process.h"
#include "content/renderer/media/video_capture_impl.h"

namespace content {

struct VideoCaptureImplManager::DeviceEntry {
    media::VideoCaptureSessionId session_id;

    // To be used and destroyed only on the IO thread.
    std::unique_ptr<VideoCaptureImpl> impl;

    // Number of clients using |impl|.
    int client_count;

    // This is set to true if this device is being suspended, via
    // VideoCaptureImplManager::Suspend().
    // See also: VideoCaptureImplManager::is_suspending_all_.
    bool is_individually_suspended;

    DeviceEntry()
        : session_id(0)
        , client_count(0)
        , is_individually_suspended(false)
    {
    }
    DeviceEntry(DeviceEntry&& other) = default;
    DeviceEntry& operator=(DeviceEntry&& other) = default;
    ~DeviceEntry() = default;
};

VideoCaptureImplManager::VideoCaptureImplManager()
    : next_client_id_(0)
    , render_main_task_runner_(base::ThreadTaskRunnerHandle::Get())
    , is_suspending_all_(false)
    , weak_factory_(this)
{
}

VideoCaptureImplManager::~VideoCaptureImplManager()
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    if (devices_.empty())
        return;
    // Forcibly release all video capture resources.
    for (auto& entry : devices_) {
        ChildProcess::current()->io_task_runner()->DeleteSoon(FROM_HERE,
            entry.impl.release());
    }
    devices_.clear();
}

base::Closure VideoCaptureImplManager::UseDevice(
    media::VideoCaptureSessionId id)
{
    DVLOG(1) << __func__ << " session id: " << id;
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    if (it == devices_.end()) {
        devices_.push_back(DeviceEntry());
        it = devices_.end() - 1;
        it->session_id = id;
        it->impl = CreateVideoCaptureImplForTesting(id);
        if (!it->impl)
            it->impl.reset(new VideoCaptureImpl(id));
    }
    ++it->client_count;

    // Design limit: When there are multiple clients, VideoCaptureImplManager
    // would have to individually track which ones requested suspending/resuming,
    // in order to determine whether the whole device should be suspended.
    // Instead, handle the non-common use case of multiple clients by just
    // resuming the suspended device, and disable suspend functionality while
    // there are multiple clients.
    if (it->is_individually_suspended)
        Resume(id);

    return base::Bind(&VideoCaptureImplManager::UnrefDevice,
        weak_factory_.GetWeakPtr(), id);
}

base::Closure VideoCaptureImplManager::StartCapture(
    media::VideoCaptureSessionId id,
    const media::VideoCaptureParams& params,
    const VideoCaptureStateUpdateCB& state_update_cb,
    const VideoCaptureDeliverFrameCB& deliver_frame_cb)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());

    // This ID is used to identify a client of VideoCaptureImpl.
    const int client_id = ++next_client_id_;

    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE,
        base::Bind(&VideoCaptureImpl::StartCapture,
            base::Unretained(it->impl.get()), client_id, params,
            state_update_cb, deliver_frame_cb));
    return base::Bind(&VideoCaptureImplManager::StopCapture,
        weak_factory_.GetWeakPtr(), client_id, id);
}

void VideoCaptureImplManager::RequestRefreshFrame(
    media::VideoCaptureSessionId id)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE,
        base::Bind(&VideoCaptureImpl::RequestRefreshFrame,
            base::Unretained(it->impl.get())));
}

void VideoCaptureImplManager::Suspend(media::VideoCaptureSessionId id)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    if (it->is_individually_suspended)
        return; // Device has already been individually suspended.
    if (it->client_count > 1)
        return; // Punt when there is >1 client (see comments in UseDevice()).
    it->is_individually_suspended = true;
    if (is_suspending_all_)
        return; // Device should already be suspended.
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE, base::Bind(&VideoCaptureImpl::SuspendCapture, base::Unretained(it->impl.get()), true));
}

void VideoCaptureImplManager::Resume(media::VideoCaptureSessionId id)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    if (!it->is_individually_suspended)
        return; // Device was not individually suspended.
    it->is_individually_suspended = false;
    if (is_suspending_all_)
        return; // Device must remain suspended until all are resumed.
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE, base::Bind(&VideoCaptureImpl::SuspendCapture, base::Unretained(it->impl.get()), false));
}

void VideoCaptureImplManager::GetDeviceSupportedFormats(
    media::VideoCaptureSessionId id,
    const VideoCaptureDeviceFormatsCB& callback)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE, base::Bind(&VideoCaptureImpl::GetDeviceSupportedFormats, base::Unretained(it->impl.get()), callback));
}

void VideoCaptureImplManager::GetDeviceFormatsInUse(
    media::VideoCaptureSessionId id,
    const VideoCaptureDeviceFormatsCB& callback)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE, base::Bind(&VideoCaptureImpl::GetDeviceFormatsInUse, base::Unretained(it->impl.get()), callback));
}

std::unique_ptr<VideoCaptureImpl>
VideoCaptureImplManager::CreateVideoCaptureImplForTesting(
    media::VideoCaptureSessionId session_id) const
{
    return std::unique_ptr<VideoCaptureImpl>();
}

void VideoCaptureImplManager::StopCapture(int client_id,
    media::VideoCaptureSessionId id)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    ChildProcess::current()->io_task_runner()->PostTask(
        FROM_HERE, base::Bind(&VideoCaptureImpl::StopCapture, base::Unretained(it->impl.get()), client_id));
}

void VideoCaptureImplManager::UnrefDevice(
    media::VideoCaptureSessionId id)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    const auto it = std::find_if(
        devices_.begin(), devices_.end(),
        [id](const DeviceEntry& entry) { return entry.session_id == id; });
    DCHECK(it != devices_.end());
    DCHECK_GT(it->client_count, 0);
    --it->client_count;
    if (it->client_count > 0)
        return;
    ChildProcess::current()->io_task_runner()->DeleteSoon(FROM_HERE,
        it->impl.release());
    devices_.erase(it);
}

void VideoCaptureImplManager::SuspendDevices(bool suspend)
{
    DCHECK(render_main_task_runner_->BelongsToCurrentThread());
    if (is_suspending_all_ == suspend)
        return;
    is_suspending_all_ = suspend;
    for (auto& entry : devices_) {
        if (entry.is_individually_suspended)
            continue; // Either: 1) Already suspended; or 2) Should not be resumed.
        ChildProcess::current()->io_task_runner()->PostTask(
            FROM_HERE, base::Bind(&VideoCaptureImpl::SuspendCapture, base::Unretained(entry.impl.get()), suspend));
    }
}

} // namespace content
